<script lang="ts">
import {defineComponent, PropType} from 'vue'
import {markRaw} from "vue";
import * as THREE from "three";
import {Scene} from "three/src/scenes/Scene";
import {WebGLRenderer} from "three/src/renderers/WebGLRenderer";
import {OrbitControls} from "three/addons/controls/OrbitControls.js";

// @ts-ignore
export default defineComponent({
  name: "MusicVudio",
  props: {
    barType: {
      type: String as PropType<'round'|'square'>,
      default: ()=>'round'
    },
  },
  data() {
    return {
      height: 0,
      width: 0,
      audio: null,
      dataArray: null,
      analyser: null,
      scene: null as Scene,
      camera: null,
      renderer: null as WebGLRenderer,
      controls: null,
      bars: markRaw([]),
      barWidth: 5,
      barCount: 128,
      animateNumber: null,
    }
  },
  watch: {
    barType: {
      handler() {
        this.$nextTick(()=>{
          this.destroy()
          this.initThreeJS()
        })
      },
      immediate: true
    }
  },
  beforeUnmount() {
    this.audio = null
    this.destroy()
  },
  mounted() {
    this.destroy()
    this.initThreeJS()
    if(this.audio) {
      this.init(this.audio)
    }
    window.$vm1= this
  },
  methods: {
    destroy() {
      this.pause()
      if(this.renderer) {
        this.renderer.domElement.remove()
        this.renderer.clear()
      }
    },
    init(audio:HTMLAudioElement | MediaStream) {
      this.$nextTick(()=>{
        this.audio = markRaw(audio)
        this.initAudioVisualization()
      })
    },
    initAudioVisualization() {
      let audioContext = new (window.AudioContext || window.webkitAudioContext)();
      let analyser = this.analyser = markRaw(audioContext.createAnalyser());
      const source = audioContext.createMediaElementSource(this.audio);
      source.connect(analyser);
      analyser.connect(audioContext.destination);
      analyser.fftSize = 256;
      const bufferLength = analyser.frequencyBinCount;
      this.dataArray = markRaw(new Uint8Array(bufferLength));
    },
    initThreeJS () {
      let scene = this.scene = markRaw(new THREE.Scene());
      let barHeight = 1
      if(this.barType === 'square') {
        barHeight = this.height;
        this.barWidth = this.width / this.barCount;
        this.camera = markRaw(new THREE.OrthographicCamera(0, this.width, this.height, 0, 0, 1000));
        this.camera.position.set(0, 0, 10); // 设置相机默认位置
        this.camera.lookAt(0, 0, 0); // 相机看向柱子围成的圈中心
      } else {
        this.barWidth = 0.5
        this.camera = markRaw(new THREE.PerspectiveCamera(75, this.width / this.height, 0.1, 1000));
        // 设置初始相机位置，让它朝上看
        this.camera.position.set(-4, 6, 13); // 设置相机默认位置
        this.camera.lookAt(0, 0, 0); // 相机看向柱子围成的圈中心
      }
      let camera = this.camera
      let renderer = this.renderer = markRaw(new THREE.WebGLRenderer({
        alpha: true, // 允许透明背景
        antialias: true // 抗锯齿，可选
      }));
      this.renderer.setSize(this.width, this.height);
      this.$refs.showEl.appendChild(renderer.domElement);
      // 创建音频可视化的柱子
      this.bars = markRaw([])
      for (let i = 0; i < this.barCount; i++) {
        const geometry = new THREE.BoxGeometry(this.barWidth, barHeight, this.barWidth);
        // 使用HSV颜色，使得每个柱子颜色不同
        const color = new THREE.Color(`hsl(${(i / this.barCount) * 360}, 100%, 50%)`);
        const material = new THREE.MeshBasicMaterial({
          color,
          opacity: 0.1
        });
        const bar = markRaw(new THREE.Mesh(geometry, material));
        // @ts-ignore
        scene.add(bar);
        this.bars.push(bar);
      }
      this.updateBarPos()
      this.createdControls()
      this.play()
    },
    createdControls() {
      if(this.barType !== 'square') {
        // 添加相机控制器
        let controls = this.controls = markRaw(new OrbitControls(this.camera, this.renderer.domElement));
        controls.enableDamping = true; // 启用阻尼
        controls.dampingFactor = 0.25; // 阻尼系数
      }
    },
    updateBarPos() {
      if(this.barType === 'round') {
        let radius = 5
        for (let i = 0; i < this.bars.length; i++) {
          const angle = (i / this.bars.length) * Math.PI * 2; // 计算每个柱子的角度
          let bar = this.bars[i]
          bar.position.x = radius * Math.cos(angle); // 根据角度计算X位置
          bar.position.z = radius * Math.sin(angle); // 根据角度计算Z位置
        }
      } else if(this.barType === 'square') {
        this.barWidth = this.width / this.barCount
        this.camera.position.set(0, 0, this.barWidth); // 设置相机默认位置
        for (let i = 0; i < this.bars.length; i++) {
          let bar = this.bars[i] as THREE.Mesh
          bar.geometry.dispose()
          bar.geometry =  new THREE.BoxGeometry(this.barWidth, this.height / 2, this.barWidth);
          bar.position.x = i * this.barWidth;
          bar.position.z = 0;
        }
      }
    },
    play() {
      if(this.animateNumber) {
        return
      }
      if(!this.analyser) {
        return;
      }
      this.animate()
    },
    pause() {
      cancelAnimationFrame(this.animateNumber)
      this.animateNumber = null
    },
    animate () {
      this.animateNumber = requestAnimationFrame(this.animate);
      this.analyser.getByteFrequencyData(this.dataArray);
      // 更新柱子的高度
      for (let i = 0; i < this.bars.length; i++) {
        const barHeight = this.dataArray[i] / 255 * 5; // 调整柱子高度
        this.bars[i].scale.y = barHeight;
        if(this.barType !== 'square') {
          this.bars[i].position.y = barHeight / 2; // 调整柱子位置
        }
      }
      if(this.barType !== 'square') {
        this.controls.update(); // 更新相机控制器
      }
      this.renderer.render(this.scene, this.camera);
    },
    onResize({height,width}) {
      this.height = height
      this.width = width
      let camera = this.camera;
      if(camera){
        if(this.barType === 'square') {
          let a = camera as THREE.OrthographicCamera
          a.left = 0
          a.right = width
          a.top = height
          a.bottom = 0
        } else {
          camera.aspect = this.width / this.height;
        }
        camera.updateProjectionMatrix();
      }
      if(this.renderer) {
        this.renderer.setSize(this.width, this.height);
      }
      this.updateBarPos()
    },
  }
})
</script>

<template>
  <el-auto-resizer class="MusicVudio" :onResize="onResize">
    <div class="content">
      <div ref="showEl"></div>
      <div class="abs" v-if="$slots.default">
        <slot></slot>
      </div>
    </div>
  </el-auto-resizer>
</template>

<style scoped lang="scss">
.MusicVudio{
  .content{
    position: relative;
    height: 100%;
    width: 100%;
    .abs{
      position: absolute;
      top: 0;
      left: 0;
      height: 100%;
      width: 100%;
    }
  }
}
</style>
